Skip to content

Z06-02 专题-vitepress

[TOC]

环境搭建

安装

TIP

环境:Node.js 18 及以上版本

版本:vitepress@1.3.1

1、安装 vitepress

sh
$ pnpm add -D vitepress

2、安装向导

sh
$ pnpm vitepress init

将需要回答几个简单的问题:

sh
  Welcome to VitePress!

  Where should VitePress initialize the config?
  ./

  Site title:
  mr笔记

  Site description:


  Theme:
 Default Theme (Out of the box, good-looking docs)
 Default Theme + Customization
 Custom Theme

文件结构

因为选择在./目录下建设站点,目录结构如下

sh
.
  api-examples.md
  index.md
  markdown-examples.md
  vitepress.md

  package.json
  pnpm-lock.yaml

└─.vitepress # 当前目录所在的位置就是文档的根目录
  config.mjs # 项目的配置文件,最重要

    └─theme # 主题目录
            index.js
            style.css

配置文件

配置文件 (.vitepress/config.js) 让你能够自定义 VitePress 站点的各个方面,最基本的选项是站点的标题和描述:

js
// .vitepress/config.js
export default {
  // 站点级选项
  title: 'VitePress',
  description: 'Just playing around.',

  themeConfig: {
    // 主题级选项
  }
}

Markdown 扩展

frontmatter

  • titlestring,页面的标题。它与 config.title 相同,并且覆盖站点级配置。

  • titleTemplatestring | boolean,标题的后缀。它与 config.titleTemplate 相同,它会覆盖站点级别的配置。

  • descriptionstring,页面的描述。它与 config.description 相同,它会覆盖站点级别的配置。

  • headHeadConfig[],指定要为当前页面注入的额外 head 标签。将附加在站点级配置注入的头部标签之后。

  • layoutdoc | home | page默认:doc,指定页面的布局。

    • doc:它将默认文档样式应用于 markdown 内容。
    • home:“主页”的特殊布局。可以添加额外的选项,例如 herofeatures,以快速创建漂亮的落地页。
    • page:表现类似于 doc,但它不对内容应用任何样式。当想创建一个完全自定义的页面时很有用。
  • herohome page only,当 layout 设置为 home 时,定义主页 hero 部分的内容。

  • featureshome page only,定义当layout 设置为 home 时要在 features 部分中显示的项目。

  • navbarboolean默认:true,是否显示导航栏。

  • sidebarboolean默认:true,是否显示侧边栏。

  • asideboolean | 'left'默认:true,定义侧边栏组件在 doc 布局中的位置。

    • 将此值设置为 false 可禁用侧边栏容器。
    • 将此值设置为 true 会将侧边栏渲染到右侧。
    • 将此值设置为 left 会将侧边栏渲染到左侧。
  • outlinenumber | [number, number] | 'deep' | false默认:2,大纲中显示的标题级别。它与 config.themeConfig.outline.level 相同,它会覆盖站点级的配置。

  • lastUpdatedboolean | Date默认:true,是否在当前页面的页脚中显示最后更新时间的文本。如果指定了日期时间,则会显示该日期时间而不是上次 git 修改的时间戳。

  • editLinkboolean默认:true,是否在当前页的页脚显示编辑链接

  • footerboolean默认:true,是否显示页脚

  • pageClassstring,将额外的类名称添加到特定页面。然后可以在 .vitepress/theme/custom.css 文件中自定义该特定页面的样式。

    css
    .custom-page-class {
      /* 特定页面的样式 */
    }

注意: 以上的配置可以通过$frontmatter访问

md
---
title: Hello
---

# {{ $frontmatter.title }}

示例:

yaml
---
title: 文章标题

titleTemplate: Vite & Vue powered static site generator

description: VitePress

head:
  - - meta
    - name: description
      content: hello
  - - meta
    - name: keywords
      content: super duper SEO

layout: doc

hero:
  name: VitePress
  text: Vite & Vue powered static site generator.
  tagline: Lorem ipsum...
  image:
    src: /logo.png
    alt: VitePress
  actions:
    - theme: brand
      text: Get Started
      link: /guide/what-is-vitepress
    - theme: alt
      text: View on GitHub
      link: https://github.com/vuejs/vitepress

features:
  - icon: 🛠️
    title: Simple and minimal, always
    details: Lorem ipsum...
  - icon:
      src: /cool-feature-icon.svg
    title: Another cool feature
    details: Lorem ipsum...
  - icon:
      dark: /dark-feature-icon.svg
      light: /light-feature-icon.svg
    title: Another cool feature
    details: Lorem ipsum...

navbar: true

sidebar: true

aside: true

lastUpdated: true

editLink: true

footer: true

pageClass: custom-page-class
---

TOC

输入:

md
[[toc]]

输出:

目录列表

配置:

可以使用 markdown.toc 选项配置 TOC 的呈现效果。

js
  markdown: {
    toc: {
      level: [2, 3, 4, 5, 6, 7, 8]
    }
  },

自定义容器

info

md
::: info 自定义标题
This is an info box.
:::

> [!NOTE] 自定义标题
> 强调用户在快速浏览文档时也不应忽略的重要信息。

image-20240719214135509

tip

md
::: tip 自定义标题
This is a tip.
:::

> [!TIP] 自定义标题
> 有助于用户更顺利达成目标的建议性信息。

image-20240719214156334

important

md
> [!IMPORTANT] 自定义标题
> 对用户达成目标至关重要的信息。

image-20240719214857402

warning

md
::: warning 自定义标题
This is a warning.
:::

> [!WARNING] 自定义标题
> 因为可能存在风险,所以需要用户立即关注的关键内容。

image-20240719214203718

danger

md
::: danger 自定义标题
This is a dangerous warning.
:::

> [!CAUTION] 自定义标题
> 行为可能带来的负面影响。

image-20240719214210199

details

md
::: details 自定义标题
This is a details block.
:::

image-20240719214220629

全局设置自定义标题

可以通过在站点配置中添加以下内容 markdown.container.tipLabel 来全局设置自定义标题,如果不是用英语书写,这会很有帮助:

js
// config.ts
export default defineConfig({
  // ...
  markdown: {
    container: {
      tipLabel: '提示',
      warningLabel: '警告',
      dangerLabel: '危险',
      infoLabel: '信息',
      detailsLabel: '详细信息'
    }
  }
  // ...
})

语法高亮

常用的高亮语言:

  • c
  • cppc++
  • css
  • html
  • http
  • java
  • javascriptjs
  • typescriptts
  • json
  • jsx
  • tsx
  • less
  • sass
  • scss
  • markdownmd
  • php
  • postcss
  • python,py
  • regexpregex
  • rubyrb
  • shellscriptbashshshell
  • sql
  • stylus,styl
  • vue
  • wasm
  • xml
  • yamlyml

行高亮

行高亮

yml
# 单行
```js {4}
```

# 多个单行
```js {4,7,9}
```

# 多行
```js {4-8}
```

# 多行与单行
```js {4,7-13,16,23-27,40}
```

!code

[!code highlight]: 行高亮

yml

# 行高亮
```js
export default {
  data () {
    return {
      msg: 'Highlighted!'
    }
  }
}
```

[!code focus]: 聚焦

  • [!code focus]
  • [!code focus:<lines>]
yml
# 聚焦
```js
export default {
  data () {
    return {
      msg: 'Focused!'
    }
  }
}
```

[!code --]: 颜色差异

  • [!code --]
  • [!code ++]
yml
# 颜色差异
```js
export default {
  data () {
    return {
      msg: 'Removed'
      msg: 'Added'
    }
  }
}
```

[!code warning]: 警告

[!code error]: 错误

yml
# 错误,警告
```js
export default {
  data () {
    return {
      msg: 'Error', 
      msg: 'Warning'
    }
  }
}
```

单词高亮 *@

依赖包:@shikijs/transformers

配置.vitepress\config.mjs

js
import { transformerNotationWordHighlight, transformerMetaWordHighlight } from '@shikijs/transformers'

export default defineConfig({
  markdown: {
    codeTransformers: [
      transformerNotationWordHighlight(), // [!code word:Hello]
      transformerMetaWordHighlight() // ```js /Hello/
    ]
  }
})

方式一:注释高亮(transformerNotationWordHighlight

语法

在后续代码中使用 [!code word:Hello] 高亮Hello

js
// 1. 基本语法:默认影响多行
[!code word:xxx]

// 2. 指定影响行数
[!code word:xxx:1]

特性

  1. 默认影响多行:在后续多行都会生效(第 2/3 行中的 Hello 都会高亮)

    image-20251229110837279

  2. 编写位置在当前行末尾:默认影响多行(包括当前行)

    image-20251229110541259

  3. 指定影响行数

    可以指定要高亮单词的行数,例如 [!code word:Hello:1] 只会在下一行高亮出现的 Hello

    image-20251229105913608


方式二:元字符高亮(transformerMetaWordHighlight

语法

根据代码摘要中提供的元字符串高亮词汇。

js
// 1. 基本语法:默认影响多行
```js /Hello/

// 2. 同时高亮多个单词
```js /Hello|World/ // 仅支持英文
```js /Hello/ /World/ // 支持中文

// 3. 注意:无法指定影响行数

示例:基本语法:默认影响多行

image-20251229111507890


注意事项

  1. 功能和markit-down冲突

代码装饰

https://shiki.tmrs.site/guide/decorations

行号

可以通过以下配置为每个代码块启用行号:

js
export default {
  markdown: {
    lineNumbers: true
  }
}

导入代码片段

可以通过下面的语法来从现有文件中导入代码片段:

  • @/: 表示绝对路径(也可以使用相对路径)

  • #snippet:表示代码文件中自定义的区域名

    js
    // #region snippet
    function foo() {
      // ..
    }
    // #endregion snippet
    
    export default foo
  • {highlightLines}: 表示行高亮

  • {highlightLines js}:中的 js 表示指定的语言

md
<<< @/filepath#snippet{highlightLines js}

代码组

可以像这样对多个代码块进行分组:

md
:::code-group

```js [config.js]
/**
 * @type {import('vitepress').UserConfig}
 */
const config = {
  // ...
}

export default config
```

```ts [config.ts]
import type { UserConfig } from 'vitepress'

const config: UserConfig = {
  // ...
}

export default config
```

:::

image-20240719222838333

包含 markdown 文件

可以像这样在一个 markdown 文件中包含另一个 markdown 文件,甚至是内嵌的。

yml
# Docs

## Basics

# 基本使用
<!--@include: ./parts/basics.md-->

# 使用绝对路径
<!--@include: @/docs/parts/basics.md-->

# 选择行范围,格式可以是:{3,}、 {,10}、{1,10}
<!--@include: ./parts/basics.md{3,}-->

parts/basics.md文件:

md
Some getting started stuff.

### Configuration

Can be created using `.foorc.json`.

图床迁移(七牛-R2)

阶段一:搬运图片数据

使用 rclone 将七牛云(Kodo)的图片迁移到 Cloudflare R2 是最稳健、最高效的方案。因为七牛云支持标准的 S3 协议,而 R2 也兼容 S3,所以这本质上是两个 S3 存储桶之间的数据同步

以下是详细的操作步骤:

第一步:准备工作

你需要先收集好两边的“钥匙”和“地址”。

七牛云端 (源)
  • Access Key (AK) / Secret Key (SK): 在七牛后台 -> 个人中心 -> 密钥管理中获取。
  • S3 Endpoint (区域域名): 根据你的存储空间所在的区域(华东、华北等),找到对应的 S3 域名(例如 s3-cn-east-1.qiniucs.com)。如果不知道,请参考上一条回答或在七牛后台“空间设置”中查看。
Cloudflare R2 端 (目标)
  • 创建 Bucket: 确保你已经建立好了 R2 存储桶(例如叫 my-blog-assets)。

  • 获取 R2 API Token:

    1. R2 首页右侧点击 "Manage R2 API Tokens"

    2. 点击 "Create API token"

    3. Permissions: 必须选择 Admin Read & Write

    4. 创建后,记下 Access Key IDSecret Access KeyEndpoint

      注意:R2 的 Endpoint 格式通常是 https://<ACCOUNT_ID>.r2.cloudflarestorage.com (不需要带 bucket 名)。

  • 忘记密钥

    • 密钥只显示一次:出于安全原因,Cloudflare R2 的 Secret Access Key 仅在创建完成的那一刻显示一次。一旦您关闭了那个弹窗页面,或者刷新了浏览器,这个密钥就永远无法再次从控制台查看了。

    • 解决办法:如果您忘记了密钥,或者之前没有保存下来,唯一的解决办法是重置(Roll)该令牌创建一个新令牌。重置操作步骤如下:

      1. 找到令牌管理入口

        1. 登录 Cloudflare Dashboard。
        2. 点击左侧菜单的 R2
        3. 在 R2 概览页面的右侧,点击蓝色的文字链接 "Manage R2 API Tokens"(管理 R2 API 令牌)。
      2. 重置令牌 (获取新密钥)

        如果您想继续使用这个令牌(不想改代码里的 Token ID,或者只想更新密钥):

        1. 在列表中找到您之前的令牌。
        2. 点击最右侧的 三个点 (...) 图标。
        3. 选择 "Roll" (重置/轮换)。
        4. 确认后,系统会生成一对新的 Access Key ID 和 Secret Access Key。
        5. ⚠️ 这一次请务必立即复制并保存到安全的地方(如密码管理器或本地 .env 文件)!
      3. 或者:创建新令牌

        如果您觉得重置太麻烦,也可以直接删掉旧的,建个新的:

        1. 点击右上角的 "Create API token"
        2. Token name: 起个名字(如 rclone-migration)。
        3. Permissions: 务必选择 Admin Read & Write(管理员读写),因为迁移数据需要创建 Bucket 和写入文件。
        4. 点击 Create API Token
        5. 立刻复制显示出来的 Access Key IDSecret Access Key

第二步:配置 rclone

打开你的终端(Terminal/CMD),输入以下命令进入交互式配置:

bash
rclone config

你需要配置两个 Remote(远程端点):一个叫 qiniu,一个叫 r2

配置七牛云 (源)

输入 n 新建,命名为 qiniu

sh
No remotes found - make a new one
n) New remote
name> qiniu

Type of storage to configure.
Choose a number from below...
xx) Amazon S3 Compliant Storage
Storage> s3  <-- 选择 s3 (通常是输入 s3 对应的数字,或者直接输 s3)

Choose your S3 provider.
provider> Qiniu <-- 选择 Qiniu (如果没有这个选项,选 Other)

Get AWS credentials from runtime (env vars or IAM).
env_auth> false <-- 输入 false

AWS Access Key ID.
access_key_id> <你的七牛AK>

AWS Secret Access Key.
secret_access_key> <你的七牛SK>

Endpoint for Qiniu Object Storage.
endpoint> s3-cn-east-1.qiniucs.com <-- 输入你的七牛 S3 域名 (不要带 http://)

...后续选项保持默认(回车)即可,ACL 可以选 public-read...
配置 R2 (目标)

再次输入 n 新建,命名为 r2

sh
n) New remote
name> r2

Type of storage to configure.
Storage> s3

Choose your S3 provider.
provider> Cloudflare

Get AWS credentials from runtime.
env_auth> false

AWS Access Key ID.
access_key_id> <你的 R2 Access Key ID>

AWS Secret Access Key.
secret_access_key> <你的 R2 Secret Access Key>

Endpoint for Cloudflare R2.
endpoint> https://<你的账号ID>.r2.cloudflarestorage.com

...后续选项保持默认...

第三步:测试连接

在正式迁移前,先列出文件看看配置对不对。

sh
# 列出七牛云 bucket 中的文件 (假设 bucket 叫 qiniu-blog)
rclone ls qiniu:qiniu-blog

# 列出 R2 bucket 中的文件 (假设 bucket 叫 r2-blog)
rclone ls r2:r2-blog

如果能看到文件列表,说明配置成功。

第四步:执行迁移 (Copy)

使用 copy 命令将七牛云的文件复制到 R2。

命令格式: rclone copy 源:源桶名 目标:目标桶名 [参数]

sh
rclone copy qiniu:my-old-bucket r2:my-new-bucket --progress --transfers=8
  • --progress: 显示进度条(推荐)。
  • --transfers=8: 设置并发数为 8(可以根据网速调整,七牛对并发太高可能会限制,一般 4-8 比较稳妥)。
  • --dry-run (可选): 如果你怕出错,可以先加上这个参数,它只会模拟运行,不会真的复制数据。

第五步:验证与收尾

  1. 检查文件: 迁移完成后,去 R2 后台或者用 rclone ls r2:my-new-bucket 检查文件数量是否一致。
  2. 设置 R2 公开访问: 确保你的 R2 Bucket 绑定了自定义域名(如 assets.yourdomain.com),否则图片无法在网页中显示。
  3. 替换链接: 按照上一个问题的回答,在 VS Code 中全局替换你的笔记中的域名。

⚠️ 费用预警

  • R2 端: 主要是写入操作费(很便宜)和存储费(前 10GB 免费),入站流量免费
  • 七牛云端: rclone 下载数据会产生“外网流出流量”
    • 七牛云对于流出流量是收费的(通常是 0.29 元/GB 左右,具体看官网)。
    • 如果你的图床有 10GB 数据,迁移过程大概会产生 3-5 元人民币的七牛云账单。请确保七牛云账户有少量余额,以免欠费导致迁移中断。

阶段二:绑定域名与验证

  1. 绑定域名: 在 Cloudflare R2 后台,进入你的 Bucket -> Settings -> Public Access -> Custom Domains。绑定一个域名,例如 assets.yourdomain.com
  2. 验证访问: 在浏览器尝试访问一张图片:https://assets.yourdomain.com/test.png,确保能正常显示。

阶段三:替换 VitePress 笔记链接

现在图片已经到了 R2,但你的 Markdown 文件里引用的还是七牛云的域名(例如 http://p1.qiniucdn.com/...)。你需要批量替换。

使用 VS Code 全局替换:

  1. 用 VS Code 打开你的 note 项目文件夹。
  2. 按下 Ctrl + Shift + H (Win) 或 Cmd + Shift + H (Mac) 打开全局替换。
  3. 查找内容 (Search): http://你的七牛云域名.com (或者 https://...)
  4. 替换为 (Replace): https://assets.yourdomain.com (你的 R2 自定义域名)
  5. 点击 "Replace All" (全部替换)。

示例:

  • 原链接:![图](http://cdn.qiniu.com/images/2024/01.png)
  • 替换后:![图](https://assets.yourdomain.com/images/2024/01.png)

备选方案:使用 Cloudflare Super Slurper (更简单)

如果你不想用命令行,Cloudflare R2 后台自带了一个数据迁移工具,叫 Super Slurper (Data Migration)

  1. 进入 R2 后台,点击 "Data Migration"
  2. 点击 "Migrate files"
  3. Source bucket provider: 选择 S3 Compatible
  4. 填入七牛云的 S3 Endpoint、AK、SK 和桶名称。
  5. Destination: 选择你的 R2 桶。
  6. 点击开始。Cloudflare 会自动去七牛云拉取文件存入 R2。

优点: 不需要本地带宽,不需要安装软件。 缺点: 偶尔因为七牛云的 API 兼容性或网络波动导致迁移失败。

图片预览【

TODO: 添加图片预览功能:https://zichin.com/blog/1.VitePress/3.怎么给 vitepress 添加点击图片放大预览功能.html

vue

自定义组件

1、在.vitepress/theme/components/目录中创建组件Counter.vue

注意: vue 中的写法和 vue 的SFC写法一样

vue
<script setup>
import { ref } from 'vue'

const count = ref(100)
function increment() {
  count.value++
}
</script>

<template>
  <div class="counter">
    <div class="show">当前计数:{{ count }}</div>
    <button class="btn" @click="increment">+ 1</button>
  </div>
</template>

<style lang="less" scoped>
.counter {
  display: flex;
  border: 1px solid red;
  padding: 20px;

  .btn {
    background: skyblue;
    padding: 0 30px;
    margin-left: 30px;
  }
}
</style>

2、注册并使用组件

  • 全局组件:

    • 1 在.vitepress/theme/index.js中注册全局组件

      js
      import Counter from './components/Counter.vue'
      
      /** @type {import('vitepress').Theme} */
      export default {
        ...
        enhanceApp({ app, router, siteData }) {
          // 注册全局组件
          app.component('Counter', Counter)
        }
      }
    • 2 在 md 文件中使用 Counter 组件,无需 import

      md
      # index
      
      <Counter />
  • 局部组件: 直接在 md 文件中使用 import 导入,并使用

    vue
    <script setup>
    import Header from '../../.vitepress/theme/components/Header.vue'
    </script>
    
    # index
    
    <Header />

布局插槽

默认主题的 <Layout/> 组件有一些插槽,能够被用来在页面的特定位置注入内容。

1、在.vitepress/theme/index.js中配置Layout

js
import { h } from 'vue'
import DefaultTheme from 'vitepress/theme'
import MyComponent from './MyComponent.vue'

export default {
  extends: DefaultTheme,
  Layout() {
    return h(DefaultTheme.Layout, null, {
      'aside-outline-before': () => h(MyComponent)
    })
  }
}

2、默认主题布局的全部可用插槽如下:

  • layout: 'doc'(默认) 在 frontmatter 中被启用时:
    • doc-top
    • doc-bottom
    • doc-footer-before
    • doc-before
    • doc-after
    • sidebar-nav-before
    • sidebar-nav-after
    • aside-top
    • aside-bottom
    • aside-outline-before
    • aside-outline-after
    • aside-ads-before
    • aside-ads-after
  • layout: 'home'在 frontmatter 中被启用时:
    • home-hero-before
    • home-hero-info-before
    • home-hero-info
    • home-hero-info-after
    • home-hero-actions-after
    • home-hero-image
    • home-hero-after
    • home-features-before
    • home-features-after
  • layout: 'page'在 frontmatter 中被启用时:
    • page-top
    • page-bottom
  • 当未找到页面 (404) 时:
    • not-found
  • 总是启用:
    • layout-top
    • layout-bottom
    • nav-bar-title-before
    • nav-bar-title-after
    • nav-bar-content-before
    • snav-bar-content-after
    • nav-screen-content-before
    • nav-screen-content-after

功能扩展

自定义 JS 代码

问题: 打包时报错:ReferenceError: document is not defined

image-20240919083929291

分析: 这是因为在 .vitepress/theme/index.js 文件中在 node 环境中调用了document

image-20240919084107144

解决: 将自定义代码放入 enhanceApp 函数中并使用 app.mixin() 来混入生命周期钩子mounted来确保在浏览器加载后执行。

js
// 最终代码

// https://vitepress.dev/guide/custom-theme
import { h } from 'vue'
import DefaultTheme from 'vitepress/theme'
import './style.css'

/** @type {import('vitepress').Theme} */
export default {
  extends: DefaultTheme,
  Layout: () => {
    return h(DefaultTheme.Layout, null, {
      // https://vitepress.dev/guide/extending-default-theme#layout-slots
    })
  },
  enhanceApp({ app, router, siteData }) {
    // 自定义代码
    app.mixin({
      mounted() {
        let els = document.querySelectorAll('.content a')
        let hs = document.querySelectorAll('.content h1,h2,h3,h4,h5,h6,h7,h8')

        els = Array.from(els)
        hs = Array.from(hs)
        const allEls = [...els, ...hs]

        allEls.forEach((el) => {
          // console.log(el.innerHTML, /^▸/.test(el.innerHTML))
          if (/^/.test(el.innerHTML)) {
            el.style.color = 'blue'
          }
        })
      }
    })
  }
}

索引页

1、在.vitepress/theme/data/web.data.mjs中使用 createContentLoader() 方法动态加载 md 文件

js
import { createContentLoader } from 'vitepress'

export default createContentLoader('/doc/web/*.md')

2、将doc/web.md文件作为索引页,并在其中导入web.data.mjs导出的动态 md 文件列表

vue
<script setup>
import { data } from '/.vitepress/theme/data/web.data.mjs'
</script>

3、对data数据通过 formatPages() 方法进行格式转换,并将转换后的数据传递给<ArticleIndex>组件

js
<script setup>
import { data } from '/.vitepress/theme/data/web.data.mjs'
import { formatPages } from '/.vitepress/theme/utils/formatPages'
import ArticleIndex from '/.vitepress/theme/components/ArticleIndex.vue'

const files = data.map(item => item.frontmatter.title)
+ const pages = formatPages(files, 'web')
</script>

<ArticleIndex :pages="pages" />

4、实现formatPages()方法

js
export function formatPages(files, dir) {
  // console.log(files, dir)
  // 获取最大的章节号
  const lastTitle = files[files.length - 1]
  const max = Number(lastTitle.match(/^[A-Z](\d+)-/)[1])

  const pages = []
  for (let i = 0; i < max + 1; i++) {
    const page = {
      chapter: '',
      articles: []
    }
    files.forEach((item, index) => {
      const article = {}
      const num = String(i + 1).padStart(2, '0')
      // console.log(item)
      item = item?.replace(/.md$/, '')
      // console.log(item)

      if (new RegExp(`^[A-Z]${num}`, 'i').test(item)) {
        const chapter = item.match(/^[A-Z]\d+-\d+ (.*?)-/)[1]

        article.title = item
        article.url = `/doc/${dir}/${item}.html`
        if (!page.chapter) {
          page.chapter = chapter
        }
        page.articles.push(article)
      }
    })
    pages.push(page)
  }

  // console.log('pages: ', pages)
  return pages
}

5、实现<ArticleIndex>组件

vue
<script setup>
const props = defineProps({
  pages: {
    type: Array,
    default: () => []
  }
})
</script>

<template>
  <div class="article-index">
    <template v-for="page in pages" :key="page.chapter">
      <div class="section" v-if="page.chapter">
        <div class="chapter">{{ page.chapter }}</div>
        <ul class="list">
          <template v-for="article in page.articles" :key="article.url">
            <li class="item">
              <a class="title" :href="article.url">{{ article.title }}</a>
            </li>
          </template>
        </ul>
      </div>
    </template>
  </div>
</template>

<style lang="less" scoped>
ul,
li {
  margin: 0;
  // padding: 0;
  list-style: none;
}
a {
  color: #5c8ec6;
  text-decoration: none;

  &:hover {
    color: #3451b2;
  }
}

.section {
  padding-bottom: 20px;
  border-top: 1px solid #dedede;
}
.chapter {
  font-weight: 700;
  line-height: 45px;
}

.list {
  .item {
    line-height: 30px;
  }
}
</style>

自动生成侧边栏

1、在config.mjs文件中,调用 generateMenu() 方法,动态获取文档目录中的 md 文件

js
// 读取文件列表
+ let files = await generateMenu('web')
files = files.filter((item) => /^[A-Z](\d+)-/.test(item))

2、实现 generateMenu() 方法,主要方法是 fs.readdirSync()(待优化)

js
// .vitepress\theme\utils\generateMenu.js

import fs from 'fs'
import path from 'path'

// 递归读取文件夹
export default function (dir) {
  async function readFolders(folder) {
    const pages = []
    const files = await fs.readdirSync(folder, { withFileTypes: true })

    files.forEach((file) => {
      if (file.isDirectory()) {
        const newFolder = path.resolve(__dirname, file.name)
        readFolders(newFolder)
      } else {
        pages.push(file.name)
      }
    })

    return pages
  }

  return readFolders(path.join(path.resolve(), '/doc/', dir))
}

3、在config.mjs中格式化文件列表为二维数组

js
// 格式化文件列表为二维数组
const webPages = formatPages(files, 'web')
const summaryPages = formatPages(files, 'summary')

4、在config.mjs中通过 genAside() 方法格式化 webPages 数组为 sidebar 格式的数据

js
export default defineConfig({
  ...
  // 主题配置
  themeConfig: {
    nav: [
      { text: 'Web', link: '/doc/web' },
      { text: '前端常用', link: '/doc/summary' }
    ],

    sidebar: {
      '/doc/web/': genAside(webPages),
      '/doc/summary/': genAside(summaryPages)
    },

5、实现 genAside() 方法

js
export function genAside(pages) {
  return pages.map((page) => {
    return {
      text: page.chapter,
      items: page.articles.map((article) => {
        return {
          text: article.title.match(/^[A-Z]\d+-\d+ .*?-(.*?)$/)[1],
          link: article.url
        }
      })
    }
  })
}

保持当前标题可见

js
/** 保持当前标题滚动到可视区内 */
;+keepActiveElVisible()

/** 保持当前标题滚动到可视区内 */
function keepActiveElVisible() {
  let index = 0
  window.addEventListener(
    'scroll',
    throttle(() => {
      const activeEl = document.querySelector('.outline-marker')
      const asideContainerEl = document.querySelector('.aside-container') // offsetHeight,scrollHeight, scrollTo()
      const liEl = document.querySelector('.outline-link.active')

      const paddingTop = parseInt(getComputedStyle(asideContainerEl).paddingTop.replace('px', ''))
      const liHeight = liEl?.offsetHeight // 32
      const scrollTop = asideContainerEl?.scrollTop
      const clientHeight = asideContainerEl?.offsetHeight
      const activeTopRaw = parseInt(activeEl.style.top.replace('px', ''))
      const activeTop = activeTopRaw + paddingTop + liHeight * 2
      const top = activeTop - clientHeight + liHeight

      // 滚动到下方不可见
      if (scrollTop + clientHeight < activeTop) {
        asideContainerEl.scrollTo({ top, behavior: 'smooth' })
      }

      // 滚动到上方不可见
      if (scrollTop > activeTopRaw) {
        asideContainerEl.scrollTo({ top: scrollTop - liHeight, behavior: 'smooth' })
      }
    }, 200)
  )
}

注册全局方法

1、定义:.vitepress\theme\index.jsenhanceApp()方法中使用app.config.globalProperties.xxx来注册全局的方法或属性。

js
export default {
  enhanceApp({ app, router, siteData }) {
    // ...
    app.config.globalProperties.genUrl = (title, dir = 'web') => {
      return `/doc/${dir}/${encodeURIComponent(title.replace(/-API$/i, ''))}-API.html`
    }
  }
}

2、使用:md文件或其他组件中可以直接使用注册过的方法或属性,如genUrl($frontmatter.title)

md
- <a :href="`${genUrl($frontmatter.title)}#transform`"><u>transform</u></a>

image-20241231090821686

手机端中文加粗

1、下载微软雅黑字体

2、通过 transfonterFontStore 网站,将下载的字体中的粗体(小于 15M 大小)转换为 CSS 可以使用的@font-face字体(后缀为woffwoff2)。

3、下载并将转换后的woff后缀字体保存到/public/fonts/目录中

4、在.vitepress\theme\style.css文件中定义@font-face

css
@font-face {
  font-family: 'MicrosoftYaHei';
  src: url('/fonts/MicrosoftYaHei.woff') format('woff'), url('/fonts/MicrosoftYaHei.woff2') format('woff2');
  font-weight: bold;
  font-style: normal;
}

5、将<strong><s><del>标签的font-family设置为MicrosoftYaHei

css
s,
del {
  font-family: 'MicrosoftYaHei', sans-serif;
  color: #eb6900;
  text-decoration: none;
  font-weight: bold;
  font-size: 1.4rem;
}

.vp-doc strong {
  font-family: 'MicrosoftYaHei', sans-serif;
  font-weight: bold;
}

6、重新打包项目,手机端中文字体就加粗显示了。